渲染过程
浏览器的工作流程大致就是:构建 DOM 树 -> 构建 CSSOM -> 构建渲染树 -> 布局 -> 绘制
- 根据 HTML 结构生成 DOM 树
- 根据 CSS 生成 CSSOM
- 将
DOM与CSSOM合并成一个渲染树。 - 根据渲染树来布局,计算每个节点的位置。渲染和展示。
- 遇到
<script>时,会执行并阻塞渲染
DOM 树和渲染树 的区别:
- DOM 树与 HTML 标签一一对应,包括 head 和隐藏元素
- 渲染树不包括 head 和隐藏元素,每一个节点都有对应的 css 属性
重绘和回流
重绘:当渲染树中的元素外观(如:颜色)发生改变,不影响布局时,产生重绘。一般都是修改元素的属性 回流:当渲染树中的元素的布局(如:尺寸、位置、隐藏/状态状态)发生改变时,产生重绘回流
引起 Repaint 和 Reflow 的一些操作:
- 调整窗口大小
- 字体大小
- 样式表变动
- 元素内容变化,尤其是输入控件
- CSS 伪类激活,在用户交互过程中发生
- DOM 操作,DOM 元素增删、修改
- width, clientWidth, scrollTop 等布局宽高的计算
回流必将引起重绘,而重绘不一定会引起回流。
另外,在写代码的时候要避免回流和重绘:
一些属性的读取也会引起回流,比如 JS 获取 Layout 属性值(如:offsetLeft、scrollTop、getComputedStyle 等),也会引起回流。因为浏览器需要通过回流计算最新值。
如何最小化重绘(repaint)和回流(reflow)?
- 使用
visibility替换display: none,因为前者只会引起重绘,后者会引发回流(改变了布局) - 需要要对 DOM 元素进行复杂的操作时,可以先隐藏(display:"none"),操作完成后再显示。避免频繁操作 DOM。创建一个 documentFragment 或 div,在它上面应用所有 DOM 操作,最后添加到文档里。设置 display:none 的元素上操作,最后显示出来。
- 需要创建多个 DOM 节点时,使用 DocumentFragment 创建完后一次性的加入 document,或使用字符串拼接方式构建好对应 HTML 后再使用 innerHTML 来修改页面
- 避免频繁读取元素几何属性(例如 scrollTop)。绝对定位具有复杂动画的元素。
- 缓存 Layout 属性值,如:var left = elem.offsetLeft; 这样,多次使用 left 只产生一次回流
- 不要使用
table布局,可能很小的一个小改动会造成整个 table 的重新布局。避免用 table 布局(table 元素一旦触发回流就会导致 table 里所有的其它元素回流) - 尽量使用 css 属性简写,如:用 border 代替 border-width, border-style, border-color
- 避免逐条更改样式。建议集中修改样式,例如操作 className。批量修改元素样式:elem.className 和 elem.style.cssText 代替 elem.style.xxx
HTML 资源加载
style 标签写在 body 后与 body 前有什么区别?
页面加载自上而下,需要先加载样式。
写在 body 标签后,由于浏览器以逐行方式对 HTML 文档进行解析,当解析到写在尾部的样式表(外联或写在 style 标签)会导致浏览器停止之前的渲染,等待加载且解析样式表完成之后重新渲染。
为什么 JS 要放到 body 尾部?
**JS 引擎是独立于渲染引擎存在的。**我们的 JS 代码在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。JS 引擎对内联的 JS 代码会直接执行,对外部 JS 文件还要先获取到脚本、再进行执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM 和 DOM 的构建。
浏览器之所以让 JS 阻塞其它的活动,是因为它不知道 JS 会做什么改变,担心如果不阻止后续的操作,会造成混乱。
结论:
- 如果 JS 在 header 中,浏览器会阻塞并等待 JS 加载完毕并执行
- 如果 JS 在 body 尾部,浏览器会进行一次提前渲染,从而提前首屏出现时间。 可以保证让浏览器优先渲染完现有的 HTML 内容,让用 户先看到内容,体验好。
- 如果 JS 需要绑定操作 DOM,那么放在 header 中如果处理不当就不会绑定到 DOM。JS 放在底部执行时,HTML 肯定都解析成了 DOM 结构。
CSS 会阻塞 DOM 解析吗?
不管是内联还是外链的 css,都会阻碍后续的 dom 渲染,但是不会阻碍后续 dom 的解析。
为什么最好把 CSS 的<link>标签放在<head></head>之间?为什么最好把 JS 的<script>标签恰好放在</body>之前,有例外情况吗?
把<link>放在<head>中
这种做法可以让页面逐步呈现,这种做法可以防止呈现给用户空白的页面或没有样式的内容。
把<script>标签恰好放在</body>之前
脚本在下载和执行期间会阻止 HTML 解析。把<script>标签放在底部,保证 HTML 首先完成解析,将页面尽早呈现给用户。
CSS 和 JS 的位置会影响页面效率,为什么?
css 在加载过程中不会影响到 DOM 树的生成,但是会影响到 Render 树的生成,进而影响到 layout,所以一般来说,style 的 link 标签需要尽量放在 head 里面,因为在解析 DOM 树的时候是自上而下的,而 css 样式又是通过异步加载的,这样的话,解析 DOM 树下的 body 节点和加载 css 样式能尽可能的并行,加快 Render 树的生成的速度。
js 脚本应该放在底部,原因在于 js 线程与 GUI 渲染线程是互斥的关系,如果 js 放在首部,当下载执行 js 的时候,会影响渲染行程绘制页面,js 的作用主要是处理交互,而交互必须得先让页面呈现才能进行,所以为了保证用户体验,尽量让页面先绘制出来。
异步加载 js 方法
script 标签 defer 和 async 以及不加标签的区别
- async 属性:目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容。 为此,建议异步脚本不要在加载期间修改 DOM。 执行顺序:让脚本在加载完可用时立即执行, 异步脚本一定会在页面的 load 事件前执行,但可能会在 DOMContentLoaded 事件触发之前或之后执行。
<!DOCTYPE html>
<html>
<head>
<title>Example HTML Page</title>
<script type="text/javascript" async src="example1.js"></script>
<script type="text/javascript" async src="example2.js"></script>
</head>
<body>
<!-- 这里放内容 -->
</body>
</html>
- defer 属性 执行顺序:在 dom 加载完毕后执行,defer 脚本的执行会在 window.onload 之前,其他没有添加 defer 属性的 script 标签之后
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta charset="UTF-8" />
</head>
<script>
window.onload = function () {
console.log("window.onload");
};
</script>
<script src="js/defer.js" defer></script>
<script>
console.log("normal");
</script>
<body></body>
</html>
- 利用 XHR 异步加载 js 内容并执行
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta charset="UTF-8" />
</head>
<script>
var xhr = new XMLHttpRequest();
xhr.open("get", "js/defer.js", true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
eval(xhr.responseText);
}
};
</script>
<body></body>
</html>
- 动态创建 script 标签
var script = document.createElement("script");
script.src = "js/test.js";
document.head.appendChild(script);
请描述<script>、<script async>和<script defer>的区别。
<script>浏览器会立即加载并执行指定的脚本,执行结束后,HTML 解析继续。<script async>脚本的提取、执行的过程与 HTML 解析过程并行,脚本执行完毕可能在 HTML 解析完毕之前。当脚本与页面上其他脚本独立时,可以使用async,比如用作页面统计分析。<script defer>延迟执行。载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后;在加载多个 JS 脚本的时候,async 是无顺序的加载,而 defer 是有顺序的加载
什么情况阻塞渲染
首先渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。
然后当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。
当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。
当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,所以对于这种情况你可以把 script 标签放在任意位置。
对于没有任何依赖的 JS 文件可以加上 async 属性,表示 JS 文件下载和解析不会阻塞渲染。